package com.ls.utils.weixin;

import com.ls.utils.UUIDUtils;
import com.ls.utils.http.HttpUtils;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * @Author guoxile
 * @Description
 * @Date 2019/10/7 14:31
 **/
@ApiModel(value = "微信支付相关")
@Data
public class WxPayUtil {
    private static final Logger logger = LoggerFactory.getLogger(WxPayUtil.class);

    /**
     * 支付下单
     *
     * @param openId             微信openid
     * @param outTradeNo         订单号
     * @param notifyUrl          支付成功通知地址
     * @param attach
     * @param commCompanyWxModel 支付相关信息
     * @param request
     * @return
     */
    public static Map getWxPay(String openId, String outTradeNo, String price, String body, String notifyUrl, String attach, CommCompanyWxModel commCompanyWxModel, HttpServletRequest request) {
        try {
            if (StringUtils.isBlank(price)) {
                throw new Exception("价格不能为空！");
            }

            logger.info("支付下单请求参数：========== openId={}, outTradeNo={}, price={}, body={}, notifyUrl{}, attach={}", openId, outTradeNo, price, body, notifyUrl, attach);
            //拼接统一下单地址参数
            Map<String, String> paraMap = new HashMap(16);
            //获取请求ip地址
            String ip = request.getHeader("x-forwarded-for");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
            if (ip.indexOf(",") != -1) {
                String[] ips = ip.split(",");
                ip = ips[0].trim();
            }
            if (StringUtils.isEmpty(body)) {
                body = "丰劵-订单结算";
            }
            String nonceStr = UUIDUtils.generate();
            price = String.valueOf(Double.valueOf(price) * 100);
            int i = price.lastIndexOf(".");
            price = price.substring(0, i);
            if (null != attach) {
                paraMap.put("attach", attach);
            }
            paraMap.put("appid", commCompanyWxModel.getWxappid());
            paraMap.put("body", body);
            paraMap.put("mch_id", commCompanyWxModel.getWxmchid());
            paraMap.put("nonce_str", nonceStr);
            paraMap.put("openid", openId);
            paraMap.put("out_trade_no", outTradeNo);//订单号
            paraMap.put("spbill_create_ip", ip);
            paraMap.put("total_fee", price);
            paraMap.put("trade_type", "JSAPI");
            paraMap.put("notify_url", notifyUrl);// 此路径是微信服务器调用支付成功结果通知路径
            String sign = WxPayUtil.generateSignature(paraMap, commCompanyWxModel.getWxkey());
            paraMap.put("sign", sign);

            String xml = WxPayUtil.mapToXml(paraMap);//将所有参数(map)转xml格式

            // 统一下单 https://api.mch.weixin.qq.com/pay/unifiedorder
            String unifiedorderUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";

            String xmlStr = HttpUtils.sendPost(unifiedorderUrl, xml);//发送post请求"统一下单接口"返回预支付id:prepay_id

            //以下内容是返回前端页面的json数据
            String prepayId = "";//预支付id
            if (xmlStr.indexOf("SUCCESS") != -1) {
                Map<String, String> map = WxPayUtil.xmlToMap(xmlStr);
                prepayId = (String) map.get("prepay_id");
            }
            Map<String, String> payMap = new HashMap(16);
            payMap.put("appId", commCompanyWxModel.getWxappid());
            long currentTime = System.currentTimeMillis() / 1000;
            String timestamp = String.valueOf(currentTime);
            payMap.put("timeStamp", timestamp);
            payMap.put("nonceStr", nonceStr);
            payMap.put("signType", "md5");
            payMap.put("package", "prepay_id=" + prepayId);
            String paySign = WxPayUtil.generateSignature(payMap, commCompanyWxModel.getWxkey());
            payMap.put("paySign", paySign);
            return payMap;
        } catch (Exception e) {
            logger.error("getWxPay:{}", e.getStackTrace().toString());
        }
        return null;
    }

    /**
     * 支付回调
     *
     * @param wxkey
     * @param request
     * @return
     */
    public static String getWeixinPayCallBack(String wxkey, HttpServletRequest request) {
        try {
            StringBuilder sb = new StringBuilder();
            InputStream inputStream = request.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String line = "";
            while ((line = bufferedReader.readLine()) != null) {
                sb.append(line);
            }

            return analysisPayAcceptXml(wxkey, sb.toString());
        } catch (Exception e) {
            logger.error("支付接收:{}", e.getStackTrace().toString());
        }
        return "";
    }

    private static String analysisPayAcceptXml(String wxkey, String xml) {
        try {
            //xml = xml.toLowerCase();
            Map map = xmlToMap(xml);
            logger.info("xml:" + map.get("appid"));

            String appid = map.get("appid").toString();
            String bankType = map.get("bank_type").toString();
            String cashFee = map.get("cash_fee").toString();
            String deviceInfo = map.get("device_info").toString();
            String feeType = map.get("fee_type").toString();
            String isSubscribe = map.get("is_subscribe").toString();
            String mchId = map.get("mch_id").toString();
            String nonceStr = map.get("nonce_str").toString();
            String openId = map.get("openid").toString();
            String outTradeNo = map.get("out_trade_no").toString();
            String resultCode = map.get("result_code").toString();
            String returnCode = map.get("return_code").toString();
            String sign = map.get("sign").toString();
            String timeEnd = map.get("time_end").toString();
            String totalFee = map.get("total_fee").toString();
            String tradeType = map.get("trade_type").toString();
            String transactionId = map.get("transaction_id").toString();
            String couponCount = "";
            String couponFee = "";
            String couponFee0 = "";
            String couponId0 = "";
            if (map.get("coupon_count") != null) {
                couponCount = map.get("coupon_count").toString();
            }
            if (map.get("coupon_fee") != null) {
                couponFee = map.get("coupon_fee").toString();
            }
            if (map.get("coupon_fee_0") != null) {
                couponFee0 = map.get("coupon_fee_0").toString();
            }
            if (map.get("coupon_id_0") != null) {
                couponId0 = map.get("coupon_id_0").toString();
            }
            Map<String, String> paraMap = new HashMap(16);
            paraMap.put("appid", appid);
            paraMap.put("bank_type", bankType);
            paraMap.put("cash_fee", cashFee);
            if (couponCount != "") {
                paraMap.put("coupon_count", couponCount);
            }
            if (couponFee != "") {
                paraMap.put("coupon_fee", couponFee);
            }
            if (couponFee0 != "") {
                paraMap.put("coupon_fee_0", couponFee0);
            }
            if (couponId0 != "") {
                paraMap.put("coupon_id_0", couponId0);
            }
            paraMap.put("device_info", deviceInfo);
            paraMap.put("fee_type", feeType);
            paraMap.put("is_subscribe", isSubscribe);
            paraMap.put("mch_id", mchId);
            paraMap.put("nonce_str", nonceStr);
            paraMap.put("openid", openId);
            paraMap.put("out_trade_no", outTradeNo);//订单号
            paraMap.put("result_code", resultCode);
            paraMap.put("return_code", returnCode);
            paraMap.put("time_end", timeEnd);
            paraMap.put("total_fee", totalFee);
            paraMap.put("trade_type", tradeType);
            paraMap.put("transaction_id", transactionId);// 此路径是微信服务器调用支付成功结果通知路径
            String cursign = WxPayUtil.generateSignature(paraMap, wxkey);
            if (sign.equals(cursign)) {
                return outTradeNo;
            } else {
                logger.error("sign:" + sign + ";cursign:" + cursign);
            }
        } catch (Exception e) {
            logger.error("getWeixinPayCallBack{}", e.getStackTrace().toString());
        }
        return "";
    }

    /**
     * 生成签名
     *
     * @param data 待签名数据
     * @param key  API密钥
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key) throws Exception {
        return generateSignature(data, key, SignType.MD5);
    }

    /**
     * 生成签名. 注意，若含有sign_type字段，必须和signType参数保持一致。
     *
     * @param data     待签名数据
     * @param key      API密钥
     * @param signType 签名方式
     * @return 签名
     */
    public static String generateSignature(final Map<String, String> data, String key, SignType signType) throws Exception {
        Set<String> keySet = data.keySet();
        String[] keyArray = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArray);
        StringBuilder sb = new StringBuilder();
        for (String k : keyArray) {
            if ("sign".equals(k)) {
                continue;
            }
            // 参数值为空，则不参与签名
            if (StringUtils.isNotBlank(data.get(k))) {
                sb.append(k).append("=").append(data.get(k).trim()).append("&");
            }
        }
        sb.append("key=").append(key);
        if (SignType.MD5.equals(signType)) {
            return md5(sb.toString()).toUpperCase();
        } else if (SignType.HMACSHA256.equals(signType)) {
            return hmacsha256(sb.toString(), key);
        } else {
            throw new Exception(String.format("Invalid sign_type: %s", signType));
        }
    }

    /**
     * XML格式字符串转换为Map
     *
     * @param xmlStr XML字符串
     * @return XML数据转换后的Map
     * @throws Exception
     */
    public static Map<String, String> xmlToMap(String xmlStr) throws Exception {
        try {
            Map<String, String> data = new HashMap(16);
            DocumentBuilder documentBuilder = WxPayXmlUtil.newDocumentBuilder();
            InputStream stream = new ByteArrayInputStream(xmlStr.getBytes("UTF-8"));
            org.w3c.dom.Document doc = documentBuilder.parse(stream);
            doc.getDocumentElement().normalize();
            NodeList nodeList = doc.getDocumentElement().getChildNodes();
            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
                Node node = nodeList.item(idx);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    org.w3c.dom.Element element = (org.w3c.dom.Element) node;
                    data.put(element.getNodeName(), element.getTextContent());
                }
            }
            try {
                stream.close();
            } catch (Exception ex) {
                // do nothing
            }
            return data;
        } catch (Exception ex) {
            //WxPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
            throw ex;
        }

    }

    /**
     * 将Map转换为XML格式的字符串
     *
     * @param data Map类型数据
     * @return XML格式的字符串
     * @throws Exception
     */
    public static String mapToXml(Map<String, String> data) throws Exception {
        org.w3c.dom.Document document = WxPayXmlUtil.newDocument();
        org.w3c.dom.Element root = document.createElement("xml");
        document.appendChild(root);
        for (String key : data.keySet()) {
            String value = data.get(key);
            if (value == null) {
                value = "";
            }
            value = value.trim();
            org.w3c.dom.Element filed = document.createElement(key);
            filed.appendChild(document.createTextNode(value));
            root.appendChild(filed);
        }
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        DOMSource source = new DOMSource(document);
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        StringWriter writer = new StringWriter();
        StreamResult result = new StreamResult(writer);
        transformer.transform(source, result);
        String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
        try {
            writer.close();
        } catch (Exception ex) {
        }
        return output;
    }

    /**
     * 生成 md5
     *
     * @param data 待处理数据
     * @return MD5结果
     */
    public static String md5(String data) throws Exception {
        MessageDigest md = MessageDigest.getInstance("md5");
        byte[] array = md.digest(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 生成 hmacsha256
     *
     * @param data 待处理数据
     * @param key  密钥
     * @return 加密结果
     * @throws Exception
     */
    public static String hmacsha256(String data, String key) throws Exception {
        Mac sha256Hmac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKey = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
        sha256Hmac.init(secretKey);
        byte[] array = sha256Hmac.doFinal(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }
    /**
     * 枚举 微信两种加密算法 hash
     */
    public enum SignType {
        /**
         * 两种加密方式
         * */
        MD5, HMACSHA256
    }
}
